﻿package idv.cjcat.fenofsm {
	use namespace ffsm;
	
	import flash.display.*;
	import flash.errors.*;
	import flash.events.*;
	import flash.utils.*;
	
	import idv.cjcat.fenofsm.events.*;
	
	/**
	 * A <code>FSMachine</code> object representes a finite state machine.
	 * <p>
	 * A finite state machine can have multiple states and transitions.
	 * A transition is a directional relation from a state to another state with a given input.
	 * A finite state machine is always in a single state at a time.
	 * Each time a finite state machine is fed an input signal (an <code>input()</code> method call),
	 * at most one outgoing transition from the current state responds to this certain input signal.
	 * </p>
	 * <p>
	 * The transition process of one state to another involves these five (or four) events (in the order of dispatchment):
	 * <ul>
	 * <li>The origin state object dispatches an <code>StateEvent.EXIT</code> event.</li>
	 * <li>The transition object dispatches a <code>TransitionEvent.INIT</code> event.</li>
	 * <li>The transition object dispatches <code>TransitionEvent.PROGRESS</code> events repeatedly during the transition delay. This event won't be dispatched if the transition delay is zero or negative.
	 * <li>The transition object dispatches a <code>TransitionEvent.COMPLETE</code> event.</li>
	 * <li>The destination state object dispatches an <code>StateEvent.ENTER</code> event.</li>
	 * </ul>
	 * The transition process ends after all the aforementioned events are dispatched, and the finite state machine is set to a new state.
	 * </p>
	 */
	public class FSMachine {
		
		private var _stateClasses:Dictionary;
		private var _transitionClasses:Dictionary;
		
		private var _sprite:Sprite = new Sprite();
		
		private var _states:Array = new Array();
		private var _transitions:Array = new Array();
		
		/** @private */
		ffsm var _currentState:State;
		private var _initState:State;
		
		private var _stateCounter:int = 0;
		private var _transitionCounter:int = 0;
		
		/**
		 * Creates a finite state machine.
		 * @param	initStateName  The name of the initial state.
		 */
		public function FSMachine(initStateName:String = null, initStateClass:Class = null) {
			if (_stateClasses == null) {
				_stateClasses = new Dictionary();
				_transitionClasses = new Dictionary();
				
				registerStateXMLTag("state", State);
				registerTransitionXMLTag("transition", Transition);
			}
			
			if (initStateClass == null) initStateClass = State;
			
			if (_stateClasses == null) {
				registerStateXMLTag("state", State);
				registerTransitionXMLTag("transition", Transition);
			}
			
			if (!FenoFSM._used) FenoFSM._used = true;
			clear(initStateName, initStateClass);
		}
		
		/**
		 * Feeds an input to the finite state machine to change the machine's state.
		 * @param	input  The input.
		 * @return The responding transition.
		 */
		public function input(input:*):Transition {
			if (_frozen) return null;
			for each (var t:Transition in _currentState._outTransitions) {
				if (t._input === input) {
					_currentState.dispatchEvent(new StateEvent(StateEvent.EXIT, _currentState));
					if (t.delay == 0) {
						t.dispatchEvent(new TransitionEvent(TransitionEvent.INIT, t));
						t.dispatchEvent(new TransitionEvent(TransitionEvent.COMPLETE, t));
						_currentState = t._to;
						_currentState.dispatchEvent(new StateEvent(StateEvent.ENTER, _currentState));
					} else if (t.delay > 0) {
						t.dispatchEvent(new TransitionEvent(TransitionEvent.INIT, t, 0));
						_frozen = true;
						
						_frozenInitTime = getTimer();
						_waitedTransition = t;
						_sprite.addEventListener(Event.ENTER_FRAME, countDown);
					} else {
						_frozen = true;
						t._waited = true;
						t.dispatchEvent(new TransitionEvent(TransitionEvent.INIT, t));
					}
					return t;
				}
			}
			return null;
		}
		private var _frozenInitTime:int;
		private var _waitedTransition:Transition;
		private function countDown(e:Event):void {
			var time:Number = getTimer();
			if ((time - _frozenInitTime) >= (_waitedTransition.delay * 1000)) {
				_waitedTransition.dispatchEvent(new TransitionEvent(TransitionEvent.COMPLETE, _waitedTransition, 1));
				_sprite.removeEventListener(Event.ENTER_FRAME, countDown);
				transitionDelay(_waitedTransition);
				_waitedTransition = null;
			} else {
				_waitedTransition.dispatchEvent(new TransitionEvent(TransitionEvent.PROGRESS, _waitedTransition, (time - _frozenInitTime) / (_waitedTransition.delay * 1000)));
			}
		}
		/** @private */
		ffsm var _frozen:Boolean;
		private function transitionDelay(t:Transition):void {
			_currentState = t._to;
			_currentState.dispatchEvent(new StateEvent(StateEvent.ENTER, _currentState));
			_frozen = false;
		}
		
		/**
		 * Creates a state in the finite state machine.
		 * @param	name     The name of the state.
		 * @return The state.
		 */
		public function createState(name:String = null, stateClass:Class = null):State {
			if (stateClass == null) stateClass = State;
			
			if (getStateByName(name) === null) {
				if (name === null) {
					name = "state" + _stateCounter;
					_stateCounter++;
				}
				var s:State = new stateClass();
				s.initialize(name, this);
				_states.push(s);
				return s;
			} else {
				throw new IllegalOperationError("A state of the same name is already in this finite state machine.");
			}
			return null;
		}
		
		/**
		 * Creates a transition in the finite state machine.
		 * <p>
		 * The origin and destination states must already exist in the machine.
		 * </p>
		 * @param	from          The origin state.
		 * @param	to            The destination state.
		 * @param	input         The input the transition shall respond to.
		 * @param	name          The name of the transition.
		 * @return The transition.
		 */
		public function createTransition(from:State, to:State, input:*, name:String = null, delay:Number = 0, transitionClass:Class = null):Transition {
			if (isNaN(delay)) delay = 0;
			if (transitionClass == null) transitionClass = Transition;
			
			if ((_states.indexOf(from) >= 0) && (_states.indexOf(to) >= 0)) {
				if (getTransitionByName(name) === null) {
					var t:Transition;
					for each (t in from._outTransitions) {
						if (t._input === input) throw new IllegalOperationError("A transition from the origin state already responds to the same input.");
					}
					if (name === null) {
						name = "transition" + _transitionCounter;
						_transitionCounter++;
					}
					t = new transitionClass();
					t.initialize(from, to, input, name, delay, this);
					from._outTransitions.push(t);
					_transitions.push(t);
					return t;
				} else {
					throw new IllegalOperationError("A transition of the same name is already in this finite state machine.");
				}
			} else {
				throw new IllegalOperationError("The origin and destination states of the transition must both be in the finite state machine.");
			}
			return null;
		}
		
		/**
		 * Removes a state from the finite state machine.
		 * @param	state  The state.
		 * @return The state, <code>null</code> of such state doesn't exist in the machine.
		 */
		public function removeState(state:State):State {
			var index:int = _states.indexOf(state);
			if (index >= 0) {
				var s:State =  _states.splice(index, 1)[0];
				s._fsm = null;
				return s;
			}
			return null;
		}
		
		/**
		 * Removes a state from the finite state machine by name.
		 * @param	stateName  The name of the state.
		 * @return The state, <code>null</code> of such state doesn't exist in the machine.
		 */
		public function removeStateByName(stateName:String):State {
			var s:State = getStateByName(stateName);
			if (s != null) return removeState(s);
			return null;
		}
		
		/**
		 * Removes a transition from the finite state machine.
		 * @param	transition  The transition.
		 * @return The transition, <code>null</code> of such transition doesn't exist in the machine.
		 */
		public function removeTransition(transition:Transition):Transition {
			for each (var s:State in _states) {
				var index:int = s._outTransitions.indexOf(transition);
				if (index >= 0) {
					var t:Transition = s._outTransitions.splice(index, 1)[0];
					t._fsm = null;
					return t;
				}
			}
			return null;
		}
		
		/**
		 * Removes a transition from the finite state machine by name.
		 * @param	transitionName The name of the transition.
		 * @return The transition, <code>null</code> of such transition doesn't exist in the machine.
		 */
		public function removeTransitionsByName(transitionName:String):Transition {
			var t:Transition = getTransitionByName(transitionName);
			if (t != null) return removeTransition(t);
			return null;
		}
		
		/**
		 * Resets the finite state machine to initial condition.
		 * <p>
		 * That is, there is only one new initial state left in the machine and no transitions exist.
		 * </p>
		 * @param	initStateName
		 */
		public function clear(initStateName:String = null, initStateClass:Class = null):void {
			if (initStateClass == null) initStateClass = State;
			
			_frozen = false;
			for each (var s:State in _states) s._fsm = null;
			for each (var t:Transition in _transitions) t._fsm = null;
			_states.length = 0;
			_transitions.length = 0;
			_stateCounter = _transitionCounter = 0;
			
			_initState = _currentState = createState(initStateName, initStateClass);
		}
		
		/**
		 * Finds a state by name.
		 * @param	stateName  The name of the state.
		 * @return The state, <code>null</code> of such state doesn't exist in the machine.
		 */
		public function getStateByName(stateName:String):State {
			for each (var s:State in _states) {
				if (s._name === stateName) return s;
			}
			return null;
		}
		
		/**
		 * Finds a transition by name.
		 * @param	transitionName The name of the transition.
		 * @return The transition, <code>null</code> of such transition doesn't exist in the machine.
		 */
		public function getTransitionByName(transitionName:String):Transition {
			for each (var t:Transition in _transitions) {
				if (t._name === transitionName) return t;
			}
			return null;
		}
		
		/**
		 * The current state.
		 */
		public function get currentState():State { return _currentState; }
		
		/**
		 * Sets the current state to a new state.
		 * @param	state    The new state.
		 * @return  The new state.
		 */
		public function setCurrentState(state:State, doExit:Boolean, doEnter:Boolean):State {
			if (_frozen) return null;
			if (_states.indexOf(state) >= 0) {
				if (doExit) _currentState.dispatchEvent(new StateEvent(StateEvent.EXIT, _currentState));
				_currentState = state;
				if (doEnter) _currentState.dispatchEvent(new StateEvent(StateEvent.ENTER, _currentState));
				return state;
			} else {
				throw new IllegalOperationError("The state must be already in the finite state machine.");
			}
			return null;
		}
		
		public function get states():Array { return _states.concat(); }
		public function get transitions():Array { return _transitions.concat(); }
		
		/**
		 * The initial state.
		 */
		public function get initState():State { return _initState; }
		
		
		//XML
		//------------------------------------------------------------------------------------------------
		
		public function registerStateXMLTag(name:String, stateClass:Class):void {
			if (_stateClasses[name] == undefined) _stateClasses[name] = stateClass;
			else throw new IllegalOperationError("This state XML name is already registered.");
		}
		
		public function registerTransitionXMLTag(name:String, transitionClass:Class):void {
			if (_transitionClasses[name] == undefined) _transitionClasses[name] = transitionClass;
			else throw new IllegalOperationError("This transtion XML name is already registered.");
		}
		
		public function toXML():XML {
			var xml:XML = <FenoFSMachine/>;
			var statesNode:XML = <states/>;
			var transitionsNode:XML = <transitions/>;
			
			xml.@version = FenoFSM.VERSION;
			xml.@initState = _initState._name;
			
			var elementXML:XML;
			var node:XML;
			for each (var s:State in _states) {
				var stateNode:XML = <state/>;
				
				//merge custom XML attributes and nodes
				elementXML = s.toXML();
				for each (node in elementXML.@*) stateNode.@[node.name()] = node;
				for each (node in elementXML.*) stateNode.appendChild(node);
				
				stateNode.@name = s._name;
				statesNode.appendChild(stateNode);
			}
			for each (var t:Transition in _transitions) {
				var transitionNode:XML = <transition/>;
				
				//merge custom XML attributes and nodes
				elementXML = t.toXML();
				for each (node in elementXML.@*) transitionNode.@[node.name()] = node;
				for each (node in elementXML.*) transitionNode.appendChild(node);
				
				transitionNode.@name = t._name;
				transitionNode.@from = t._from._name;
				transitionNode.@to = t._to._name;
				transitionNode.@input = t._input.toString();
				if (t._input is Number) transitionNode.@inputType = "float";
				else if (t._input is int) transitionNode.@inputType = "int";
				else if (t._input is uint)transitionNode.@inputType = "uint";
				else if (t._input is Boolean) transitionNode.@inputType = "bool";
				else if (t._input is String) transitionNode.@inputType = "string";
				transitionNode.@delay = t.delay;
				
				transitionsNode.appendChild(transitionNode);
			}
			xml.appendChild(statesNode);
			xml.appendChild(transitionsNode);
			
			return xml;
		}
		
		public function parseXML(xml:XML):void {
			
			//parse initial state
			var initStateClass:Class;
			var node:XML;
			var initStateFound:Boolean = false;
			for each (node in xml.states.*) {
				if (xml.@initState.toString() == node.@name.toString()) {
					initStateFound = true;
					initStateClass = _stateClasses[node.name().toString()];
					break;
				}
			}
			if (!initStateFound) throw new IllegalOperationError("Initial state not declared in the state list: " + xml.@initState.toString());
			
			clear(xml.@initState, initStateClass);
			_initState.parseXML(node);
			
			//parse states
			for each (node in xml.states.*) {
				if (node.@name.toString() == _initState._name) continue;
				
				if (_stateClasses[node.name().toString()] == undefined) throw new IllegalOperationError("This state XML tag is not registered and cannot be recognized: " + node.name().toString());
				var stateClass:Class = _stateClasses[node.name().toString()];
				
				createState(node.@name, stateClass).parseXML(node);
			}
			
			//parse transitions
			for each (node in xml.transitions.*) {
				var input:*;
				if (node.@inputType == "float") input = parseFloat(node.@input);
				else if (node.@inputType == "int" || node.@inputType == "uint") input = parseInt(node.@input);
				else if (node.@inputType == "bool") input = (node.@input == "true");
				else if (node.@inputType == "string") input = node.@input.toString();
				else throw new IllegalOperationError("Invalid input type.");
				
				if (_transitionClasses[node.name().toString()] == undefined) throw new IllegalOperationError("This transition XML tag is not registered and cannot be recognized: " + node.name().toString());
				var transitionClass:Class = _transitionClasses[node.name().toString()];
				createTransition(
					getStateByName(node.@from), 
					getStateByName(node.@to), 
					input, 
					node.@name, 
					parseFloat(node.@delay),
					transitionClass
				).parseXML(node);
			}
		}
		
		//------------------------------------------------------------------------------------------------
		//end of XML
		
	}
}